Skip to content

S01-09 JavaSE-面向对象-进阶

[TOC]

概述

多个程序员开发同一个项目时,可能出现类名冲突(如两个 Dog 类),通过包区分。

包(Package):是 Java 中管理类和接口的核心机制,本质是命名空间(Namespace),用于解决类名冲突组织代码结构控制访问权限三大核心问题。它是 Java 模块化编程的基础,贯穿于类的定义、导入、编译、运行全流程。每个类都属于一个包,若未显式声明包,则属于默认包(无名包)。

包的本质命名空间与代码组织

  • 命名空间:通过包名给类名添加前缀,避免不同包下的类名冲突(如 java.util.Datejava.sql.Date);
  • 代码组织:按功能模块划分包,使代码结构清晰,便于维护(如 com.demo.controller 存放控制器、com.demo.service 存放服务类)。

包的命名规则

Java 官方规定包名采用反向域名命名法,确保全球唯一:

  • 只能包含数字/字母/下划线/点.,不能以数字开头,不能有关键字/保留字;
  • 全部小写,避免大小写敏感问题;
  • 以公司 / 组织的反向域名开头,后接功能模块;
  • 不同层级用点.分隔。
java
// 公司项目
package com.baidu.xxx
// 个人项目
package com.demo.controller
package com.demo.service

基本语法

包的声明

包的声明语法格式

java
package 包名;

核心规则

  1. 必须是源文件的第一条语句:在 package 语句之前不能有任何代码(可以有注释);
  2. 一个源文件只能有一个 package 语句:一个类只能属于一个包;
  3. 包名与目录结构一致:编译后,包名对应操作系统的目录结构(如 com.demo 对应 com/demo 目录)。

示例

java
// 正确:package 是第一条语句
package com.demo.model;

public class User {
    private String name;
}

默认包

若源文件中未显式声明 package 语句,则该类属于默认包(无名包)。

默认包特点

  • 包名为空,类名无前缀;
  • 不同源文件的默认包类之间可直接访问(默认访问权限);
  • 不推荐使用:易导致类名冲突,且无法导入其他包的类(JDK 1.4 后限制)。

示例

java
// 无 package 语句,属于默认包
public class DefaultPackageClass {}

快速入门

java
// 包com.xiaoming下的Dog类
package com.xiaoming;
public class Dog {}
java
// 包com.xiaoqiang下的Dog类
package com.xiaoqiang;
public class Dog {}
java
// 测试类
package com.use;
import com.xiaoming.Dog; // 导入指定包的类
public class Test {
    public static void main(String[] args) {
        Dog dog1 = new Dog(); // com.xiaoming.Dog
        com.xiaoqiang.Dog dog2 = new com.xiaoqiang.Dog(); // 全类名访问
    }
}

三大核心作用

包的三大核心作用

  1. 作用 1:解决类名冲突(核心作用)

    不同包下的类可以同名,通过全限定类名(包名 + 类名)区分。

    java
    // 1. 导入 java.util 包的 Date 类
    import java.util.Date;
    // 2. 导入 java.sql 包的 Date 类,用别名区分
    import java.sql.Date as SqlDate;
    
    public class DateDemo {
        public static void main(String[] args) {
            Date utilDate = new Date(); // java.util.Date
            java.sql.Date sqlDate = new java.sql.Date(System.currentTimeMillis()); // 全限定类名
        }
    }
  2. 作用 2:组织代码结构(模块化编程)

    按功能模块划分包,使代码结构清晰,符合 “高内聚、低耦合” 的设计原则。

    plaintext
    com.demo
    ├── controller  // 控制器(处理请求)
    ├── service     // 服务层(业务逻辑)
    ├── dao         // 数据访问层(操作数据库)
    ├── model       // 实体类(数据模型)
    ├── util        // 工具类(通用功能)
    └── config      // 配置类(全局配置)
  3. 作用 3:控制访问权限(与访问修饰符配合)

    包与访问修饰符(defaultprotected)配合,实现类的访问控制:

    • default 访问权限(无修饰符):仅同包内的类可访问;
    • protected 访问权限:同包内的类 + 不同包的子类可访问。
    java
    // 包 com.demo.a
    package com.demo.a;
    
    // default 权限的类:仅同包可访问
    class DefaultClass {
        public void show() {
            System.out.println("default 类的方法");
        }
    }
    
    
    public class TestA {
        public static void main(String[] args) {
            // 同包的测试类:可访问 DefaultClass
            DefaultClass dc = new DefaultClass(); // 合法
            dc.show();
        }
    }
    java
    // 不同包的测试类:不可访问 DefaultClass
    package com.demo.b;
    
    import com.demo.a.DefaultClass; // 错误:DefaultClass 是 default 权限,无法导入
    
    public class TestB {
        public static void main(String[] args) {
            // DefaultClass dc = new DefaultClass(); // 错误:无法访问
        }
    }

类的导入

通过 import 语句引入其他包的类,避免在代码中重复写全限定类名,简化代码编写。

语法格式

语法格式

  1. 格式 1:导入单个类

    java
    import 包名.类名;
  2. 格式 2:导入包下所有类(通配符 *

    java
    import 包名.*;
  3. 格式 3:静态导入(导入类的静态成员)

    java
    // 导入单个静态成员
    import static 包名.类名.静态成员名;
    // 导入所有静态成员
    import static 包名.类名.*;

核心规则

  1. import 语句的位置:必须在 package 语句之后,类定义之前;

  2. 一个源文件可有多条 import 语句:按需导入多个包的类;

  3. 通配符 * 仅导入当前包的类:不导入子包的类(如 import com.demo.* 不导入 com.demo.controller.User);

  4. 默认导入:JVM 自动导入 java.lang.* 包下的所有类(如 StringSystemObject),无需显式导入。

示例

  1. 单个类导入与全限定类名

    java
    package com.demo.test;
    
    // 1. 导入 java.util 包的 ArrayList 类
    import java.util.ArrayList;
    
    public class ImportDemo {
        public static void main(String[] args) {
            // 2. 简化写法:直接使用类名
            ArrayList<String> list1 = new ArrayList<>();
    
            // 全限定类名:不导入也可使用,但繁琐
            java.util.HashMap<String, String> map = new java.util.HashMap<>();
        }
    }
  2. 通配符导入

    java
    package com.demo.test;
    
    // 1. 导入 java.util 包下所有类
    import java.util.*;
    
    public class ImportWildcardDemo {
        public static void main(String[] args) {
            ArrayList<String> list = new ArrayList<>(); // 2. 合法
            HashMap<String, String> map = new HashMap<>(); // 3. 合法
        }
    }
  3. 静态导入

    java
    package com.demo.test;
    
    // 1. 静态导入 Math 类的 PI 常量和 sqrt 方法
    import static java.lang.Math.PI;
    import static java.lang.Math.sqrt;
    
    public class StaticImportDemo {
        public static void main(String[] args) {
            // 2. 简化写法:直接使用静态成员
            System.out.println(PI); // 3.141592653589793
            System.out.println(sqrt(4)); // 2.0
    
            // 3. 不静态导入的写法:Math.PI、Math.sqrt(4)
        }
    }

解决类名冲突

当导入的两个包中有同名类时,需通过全限定类名区分,或使用导入别名(JDK 10+ 支持)。

方式 1:全限定类名

java
package com.demo.test;

// 1. 导入 java.util.Date
import java.util.Date;

public class DateConflictDemo {
    public static void main(String[] args) {
        // 2. java.util.Date
        Date utilDate = new Date();

        // 3. 用全限定类名区分 java.sql.Date
        java.sql.Date sqlDate = new java.sql.Date(System.currentTimeMillis());
    }
}

方式 2:导入别名

java
package com.demo.test;

// 给 java.sql.Date 起别名 SqlDate
import java.sql.Date as SqlDate;
import java.util.Date;

public class DateAliasDemo {
    public static void main(String[] args) {
        Date utilDate = new Date();
        SqlDate sqlDate = new SqlDate(System.currentTimeMillis()); // 使用别名
    }
}

包的编译与运行

命令行

Java 编译器要求包名与目录结构完全一致,否则会编译失败或运行时找不到类。

命令行编译与运行步骤

  1. 步骤 1:创建目录结构

    假设包名是 com.demo,需创建目录 com/demo,将 Hello.java 放入该目录:

    plaintext
    └── com
        └── demo
            └── Hello.java
  2. 步骤 2:编写代码(Hello.java)

    java
    package com.demo;
    
    public class Hello {
        public void sayHello() {
            System.out.println("Hello, Package!");
        }
    }
  3. 步骤 3:编译代码(javac 命令)

    根目录com 目录的上级目录)执行编译命令,使用 :

    sh
    javac -d bin com/demo/Hello.java
    • -d bin:指定编译后类文件的输出目录(保持包结构),如bin/com/demo/Hello.class
  4. 步骤 4:运行代码(java 命令)

    根目录执行运行命令,使用全限定类名(包名 + 类名):

    sh
    java -cp bin com.demo.Hello
    • -cp bin:指定类路径为 bin 目录;
    • com.demo.Hello:全限定类名,不能写 Hellocom/demo/Hello

IDE

IDEA、Eclipse 等 IDE 会自动根据 package 语句创建目录结构,编译和运行时自动处理类路径,无需手动操作

IDEA 中创建包的步骤

  1. 右键 src 目录 → NewPackage
  2. 输入包名(如 com.demo.controller),IDEA 自动创建 com/demo/controller 目录;
  3. 在包下创建类,自动生成 package 语句。

Java 核心包

JDK 提供了大量内置包,包含常用的类和接口,以下是最核心的几个:

  1. java.lang:核心包,自动导入,包含 StringObjectSystemMath 等类;

  2. java.util:工具包,包含 ArrayListHashMapDateRandomScanner 等类;

  3. java.io:输入输出包,包含文件操作、流操作相关类(如 FileInputStream);

  4. java.net:网络编程包,包含 SocketServerSocket 等类;

  5. java.sql:数据库编程包,包含 ConnectionStatementResultSet 等接口;

  6. java.awt/swing:图形界面编程包,包含窗口、按钮等组件类。

访问修饰符

Java 提供 4 种访问修饰符(Modifier),其访问范围与包密切相关。以下是类和成员的访问权限对比表:

类的访问修饰符

类只能用 publicdefault(无修饰符)修饰,访问范围如下:

修饰符同包可见不同包可见核心说明
public所有包可访问,一个 .java 文件只能有一个 public 类,且类名与文件名一致
default仅同包可访问,无修饰符的类属于默认访问权限

类成员的访问修饰符

类的成员(变量、方法、内部类)可使用 4 种修饰符,访问范围如下:

修饰符本类同包不同包子类所有包核心说明
private仅本类可访问,与包无关
default仅同包可访问,无修饰符
protected同包 + 不同包子类可访问
public所有包可访问

示例

  1. 示例 1:protected 跨包访问

    protected 修饰的成员,不同包的子类只能通过子类实例访问,不能通过父类实例访问。

    java
    // 父类:com.demo.parent
    package com.demo.parent;
    
    public class Parent {
        protected String name = "Parent";
    }
    java
    // 子类:com.demo.child(不同包)
    package com.demo.child;
    
    import com.demo.parent.Parent;
    
    public class Child extends Parent { 
        public void test() {
            // 1. ✅ 合法:子类实例访问 protected 成员
            System.out.println(this.name); // Parent
    
            // 2. ✅ 合法:子类实例访问
            Child child = new Child();
            System.out.println(child.name); 
    
            // 3. ❌ 错误:不同包不能通过父类实例访问 protected 成员
            Parent parent = new Parent();
            // System.out.println(parent.name);
        }
    }
  2. 示例 2:四种访问修饰符的访问范围

    java
    package com.hspedu.modifier;
    
    public class A {
        // 修饰属性
        public int n1 = 100;
        protected int n2 = 200;
        int n3 = 300;
        private int n4 = 400;
    
        // 修饰方法
        public void m1() {}
        protected void m2() {}
        void m3() {}
        private void m4() {}
    
        public void hi() {
            // 1. ✅ 同类可访问所有属性和方法
            System.out.println(n1 + " " + n2 + " " + n3 + " " + n4);
            m1(); m2(); m3(); m4();
        }
    }
    java
    // 同包:
    package com.hspedu.modifier;
    public class B {
        public void say() {
            A a = new A();
            // 2. ✅ 同包可访问:public/protected/default属性和方法
            System.out.println(a.n1 + " " + a.n2 + " " + a.n3);
            a.m1(); a.m2(); a.m3();
    
            // 3. ❌ 同包类不可访问:private属性和方法
            // System.out.println(a.n4);
            // a.m4();
        }
    }
    java
    // 不同包:
    package com.hspedu.pkg;
    public class C {
        public void say() {
            A a = new A();
            // 2. ✅ 不同包可访问:public属性和方法
            System.out.println(a.n1);
            a.m1();
    
            // 3. ❌ 不同包不可访问:protected/default/private属性和方法
            // System.out.println(a.n2 + " " + a.n3 + "" + a.n4);
            //  a.m2(); a.m3();a.m4();
        }
    }

封装

OOP 三大特性

Java 面向对象编程的三大核心特性封装、继承、多态

三大特性是 OOP 的基石,封装为 “基础”(保护数据、隐藏细节),继承为 “手段”(复用代码、构建层次),多态为 “延伸”(灵活扩展、解耦设计),三者相辅相成,共同构建健壮、可扩展的面向对象系统。

  • 封装(Encapsulation):OOP 的基础,“隐藏细节,暴露接口”
  • 继承(Inheritance):OOP 的手段,“复用代码,构建层次”
  • 多态(Polymorphism):OOP 的延伸,“一个接口,多种实现”

封装概述

封装(Encapsulation):是指将对象的内部状态(属性)和行为(方法)捆绑在一起,隐藏内部实现细节,仅通过公开的接口与外部交互,从而实现 “信息隐藏” 和 “数据保护”。

本质信息隐藏 + 数据保护

封装的核心是 “隐藏实现,暴露接口”:

  • 信息隐藏:对外隐藏对象的内部状态和实现细节(如属性的存储方式、方法的执行逻辑),外部无法直接操作;
  • 数据保护:通过可控的接口(getter/setter)操作数据,在接口中添加校验逻辑,保证数据的合法性和一致性。

封装的核心优势

封装的核心优势

  • 数据安全

    通过 setter 中的校验逻辑,杜绝非法数据(如年龄负数、姓名为空),保证对象状态的一致性。

  • 降低耦合

    外部仅依赖公开接口,不依赖内部实现。例如:修改 Person 类的 age 属性名为 userAge,只需修改 getAge()/setAge() 的内部实现,外部调用处无需任何修改。

  • 代码可维护

    所有数据访问逻辑集中在 getter/setter 中,修改时只需改一处。例如:将年龄的校验规则从 0-150 改为 0-120,仅需修改 setAge() 方法。

  • 增强可扩展性

    可在接口中灵活添加额外逻辑,不影响外部调用:

    java
    // 扩展:在setter中添加日志
    public void setAge(int age) {
        if (age < 0 || age > 150) {
            throw new IllegalArgumentException("年龄必须在0-150之间!");
        }
        // 新增日志逻辑,外部调用无感知
        System.out.println("修改年龄:" + this.age + " → " + age);
        this.age = age;
    }
  • 提升代码复用性

    内部私有方法可被多个公共接口复用,避免代码冗余。例如:calculateDiscount() 可被 createOrder()refundOrder() 等方法复用。

封装的实现步骤

封装的实现步骤

Java 中封装的实现依赖访问修饰符(主要是 private)和公共接口(getter/setter),核心步骤如下:

  1. 步骤 1:私有化成员变量(核心)

    将类的成员变量用 private 修饰,禁止外部类直接访问,这是封装的基础。

  2. 步骤 2:提供公共的 getter/setter 方法

    IDEA 快捷键Alt + Insert

    • getter 方法:用于获取成员变量的值,命名规则 getXxx()(布尔类型可简化为 isXxx());
    • setter 方法:用于修改成员变量的值,命名规则 setXxx(参数)
    • 方法用 public 修饰,作为外部访问私有变量的唯一入口。
  3. 步骤 3:在 setter 中添加数据校验(可选但推荐)

    在 setter 方法中校验传入值的合法性,拒绝非法数据(如年龄 < 0、姓名为空),保证数据一致性。

  4. 步骤 4:在 getter 中添加权限判断(可选)

  5. 步骤 5:隐藏内部业务逻辑(进阶)

    将内部辅助方法(如计算、校验的工具方法)私有化,仅暴露对外的核心业务方法。

快速入门

对比未封装 vs 封装

  1. 未封装的 Person 类(问题暴露)

    成员变量用 public 修饰,外部可直接访问和修改,导致数据失控:

    java
    // 未封装的类:数据不安全,耦合度高
    public class UnEncapsulatedPerson {
        // 公共成员变量:外部可直接修改
        public String name;
        public int age;
    
        public static void main(String[] args) {
            UnEncapsulatedPerson person = new UnEncapsulatedPerson();
            // 问题1:可随意设置非法数据(年龄为负数)
            person.age = -20;
            // 问题2:可设置空姓名
            person.name = "";
            // 问题3:后续若修改属性名(如age改为userAge),所有外部调用处都要改
            System.out.println("姓名:" + person.name + ",年龄:" + person.age); // 姓名:,年龄:-20
        }
    }
  2. 封装后的 Person 类(最佳实践)

    私有化成员变量,提供带校验的 getter/setter,隐藏内部逻辑:

    java
    // 封装后的类:数据安全,可维护性高
    public class EncapsulatedPerson {
        // 1. 私有化成员变量:外部无法直接访问
        private String name;
        private int age;
    
        // 2. 提供 getter 方法:获取属性值
        public String getName() {
            return this.name;
        }
    
        // 3. 提供 setter 方法:修改属性值,添加数据校验
        public void setName(String name) {
            // 校验:姓名不能为空或空白字符串
            if (name == null || name.trim().isEmpty()) {
                throw new IllegalArgumentException("姓名不能为空!");
            }
            this.name = name;
        }
    
        public int getAge() {
            return this.age;
        }
    
        public void setAge(int age) {
            // 校验:年龄必须在0-150之间
            if (age < 0 || age > 150) {
                throw new IllegalArgumentException("年龄必须在0-150之间!");
            }
            this.age = age;
        }
    
        // 4. 隐藏内部业务逻辑:私有化辅助方法
        private void validatePerson() {
            System.out.println("校验人员信息:" + this.name + "," + this.age);
        }
    
        // 5. 暴露公共业务接口:外部仅能调用该方法
        public void showPersonInfo() {
            this.validatePerson(); // 内部调用私有方法
            System.out.println("姓名:" + this.name + ",年龄:" + this.age);
        }
    
        // 测试封装效果
        public static void main(String[] args) {
            EncapsulatedPerson person = new EncapsulatedPerson();
    
            // 合法数据:正常设置
            person.setName("张三");
            person.setAge(25);
            person.showPersonInfo(); // 校验人员信息:张三,25 → 姓名:张三,年龄:25
    
            // 非法数据:抛出异常,保证数据安全
            try {
                person.setName(""); // 抛出:IllegalArgumentException: 姓名不能为空!
            } catch (IllegalArgumentException e) {
                System.out.println(e.getMessage());
            }
    
            try {
                person.setAge(-5); // 抛出:IllegalArgumentException: 年龄必须在0-150之间!
            } catch (IllegalArgumentException e) {
                System.out.println(e.getMessage());
            }
        }
    }
  3. 关键对比:封装前后的差异

    维度未封装(public 变量)封装(private 变量 + getter/setter)
    数据安全外部可随意设置非法值仅能通过校验后的 setter 设置合法值
    代码耦合外部直接依赖属性名,修改属性名需改所有调用处外部依赖接口,修改属性名仅需改 getter/setter
    逻辑扩展无法添加额外逻辑(如日志、校验)可在 getter/setter 中添加日志、缓存等
    可维护性低(校验逻辑分散在各处)高(校验逻辑集中在 setter)

访问修饰符

封装的实现依赖 Java 的访问修饰符,通过不同的修饰符控制类 / 成员的可见范围,核心修饰符对比如下:

修饰符本类同包不同包子类所有类封装场景中的作用
private私有化成员变量 / 内部方法,核心封装修饰符
default同包内可见,用于包内封装
protected子类可见,用于继承场景的封装
public暴露公共接口(getter/setter/ 业务方法)

关键说明

  • 封装的核心是 private:90% 的封装场景都是将成员变量设为 private,仅暴露 public 接口;
  • default 用于包内封装:仅同包内的类可访问,适合包内复用的工具方法;
  • protected 用于继承封装:允许子类访问,但禁止外部无关类访问;
  • public 仅暴露必要接口:避免将所有方法设为 public,遵循 “最小权限原则”。

构造器结合 setter

虽然在类中设置了 getter/setter,但是在初始化对象时会调用构造器,默认情况下可以通过构造器绕过设置的 getter/setter 方法。此时就需要通过在构造器中调用 setter 方法确保初始化时也能进行数据验证。

java
public Person (String name, Int age) {
    // 在构造器中调用 setter 方法
    this.setName(name);
    this.setAge(age);
}

封装进阶场景

只读/只写属性

只读 / 只写属性按需暴露接口

封装并非必须同时提供 getter 和 setter,可根据需求仅暴露部分接口:

  • 只读属性:只提供 getter,不提供 setter(如用户 ID,创建后不可修改);
  • 只写属性:只提供 setter,不提供 getter(如密码,仅能设置,不能获取)。
java
public class User {
    // 只读属性:用户ID(创建后不可改)
    private final String userId;
    // 只写属性:密码(仅能设置,不能获取)
    private String password;

    // 构造方法初始化只读属性
    public User(String userId) {
        this.userId = userId;
    }

    // 只读:仅提供getter
    public String getUserId() {
        return this.userId;
    }

    // 只写:仅提供setter,且添加密码强度校验
    public void setPassword(String password) {
        if (password == null || password.length() < 6) {
            throw new IllegalArgumentException("密码长度不能小于6位!");
        }
        this.password = password;
    }
}

隐藏复杂业务逻辑

隐藏复杂业务逻辑

将复杂的内部逻辑私有化,仅暴露简单的公共接口,降低外部使用成本:

java
public class OrderService {
    // 私有化内部方法:计算折扣
    private double calculateDiscount(double amount, int vipLevel) {
        if (vipLevel == 1) return amount * 0.9;
        if (vipLevel == 2) return amount * 0.8;
        return amount;
    }

    // 私有化内部方法:生成订单号
    private String generateOrderId() {
        return "ORDER_" + System.currentTimeMillis();
    }

    // 暴露公共接口:创建订单(外部仅需调用该方法)
    public String createOrder(double amount, int vipLevel) {
        double finalAmount = this.calculateDiscount(amount, vipLevel);
        String orderId = this.generateOrderId();
        System.out.println("创建订单:" + orderId + ",金额:" + finalAmount);
        return orderId;
    }
}

// 外部调用:无需关心折扣计算、订单号生成的细节
class TestOrder {
    public static void main(String[] args) {
        OrderService service = new OrderService();
        service.createOrder(1000, 2); // 创建订单:ORDER_17362xxxx,金额:800.0
    }
}

不可变类(终极封装)

不可变类终极封装

不可变类是封装的极致形式:对象创建后,属性不可修改,核心特点:

  • 成员变量私有化 + final 修饰;
  • 无 setter 方法;
  • 构造方法初始化所有成员;
  • 类用 final 修饰(禁止继承,避免子类修改逻辑)。

Java 中的 StringInteger 等包装类都是不可变类:

java
// 自定义不可变类:地址类
public final class Address {
    // 私有 + final 成员变量
    private final String province;
    private final String city;

    // 构造方法初始化所有成员
    public Address(String province, String city) {
        this.province = province;
        this.city = city;
    }

    // 仅提供 getter,无 setter
    public String getProvince() {
        return province;
    }

    public String getCity() {
        return city;
    }
}

// 测试不可变类:属性无法修改
class TestAddress {
    public static void main(String[] args) {
        Address addr = new Address("广东省", "深圳市");
        System.out.println(addr.getProvince()); // 广东省
        // 无 setter,无法修改属性,保证对象不可变
    }
}

练习

需求:创建 Account 类,要求:

  • 姓名:长度 2-4 位,否则默认"无名"
  • 余额:必须>20,否则默认 0
  • 密码:必须 6 位,否则默认"000000"
  • 提供 setter/getter 方法和showInfo()方法
java
package com.hspedu.encap;

public class Account {
    // 私有化属性
    private String name;
    private double balance;
    private String pwd;

    // 构造器
    public Account() {}

    public Account(String name, double balance, String pwd) {
        this.setName(name);
        this.setBalance(balance);
        this.setPwd(pwd);
    }

    // getter 方法
    public String getName() {
        return name;
    }

    public double getBalance() {
        return balance;
    }

    public String getPwd() {
        return pwd;
    }

    // setter 方法(含验证)
    public void setName(String name) {
        if (name.length() >= 2 && name.length() <= 4) {
            this.name = name;
        } else {
            System.out.println("姓名要求(长度为2-4位),默认值无名");
            this.name = "无名";
        }
    }

    public void setBalance(double balance) {
        if (balance > 20) {
            this.balance = balance;
        } else {
            System.out.println("余额(必须>20),默认为0");
            this.balance = 0;
        }
    }

    public void setPwd(String pwd) {
        if (pwd.length() == 6) {
            this.pwd = pwd;
        } else {
            System.out.println("密码(必须是六位),默认密码为000000");
            this.pwd = "000000";
        }
    }

    // 显示账号信息
    public void showInfo() {
        // 可以添加权限校验
        System.out.println("账号信息:name=" + name + " 余额=" + balance + " 密码=" + pwd);
    }
}
java
// 测试类
package com.hspedu.encap;
public class TestAccount {
    public static void main(String[] args) {
        Account account = new Account();
        account.setName("jack");
        account.setBalance(60);
        account.setPwd("123456");
        account.showInfo(); // 账号信息:name=jack 余额=60.0 密码=123456
    }
}

继承

概述

多个类存在相同的属性和方法时,代码冗余,维护成本高。通过继承抽取父类,实现代码复用。

Pupil(小学生)和 Graduate(大学生)都有 name、age、score 属性和 testing()、showInfo()方法,可抽取父类 Student。

继承(Inheritance):是基于已有的类(父类 / 超类)创建新类(子类 / 派生类),子类复用父类的属性和方法,同时可扩展自身的功能。它是实现代码复用、构建类层次结构、支撑多态的核心机制。

本质代码复用 + 类层次化

  • 代码复用:避免重复编写相同的属性和方法(如多个子类都需要 “姓名、年龄” 属性,可抽离到父类);
  • 类层次化:构建 “通用 → 具体” 的类结构(如 AnimalMammalDog),符合现实世界的分类逻辑;
  • 多态基础:继承是多态的前提(子类对象可赋值给父类引用)。

继承关系示意图

image-20260108154450527

基本语法

基本语法

使用 extends 关键字声明继承关系,格式如下:

java
// 父类(超类/基类)
public class 父类名 {
    // 属性、方法
}

// 子类(派生类):继承父类
public class 子类名 extends 父类名 {
    // 新增属性、方法,或重写父类方法
}

核心规则

  1. 规则 1:Java 是单继承

    子类只能直接继承一个父类(避免多继承的菱形问题),但可通过 “多层继承” 间接继承多个类的特性:

    java
    // 合法:多层继承
    class A {}
    class B extends A {}
    class C extends B {} // C 间接继承 A
    
    // 错误:多继承(Java 不支持)
    // class D extends A, B {}
  2. 规则 2:所有类默认继承 Object 类

    Java 中没有显式声明父类的类,默认继承 java.lang.Object 类(Object 是所有类的根父类)。

    Object 类提供了所有对象的通用方法(如 equals()hashCode()toString()),所有类都可直接使用或重写这些方法。

    java
    // 等价于 class Person extends Object {}
    public class Person {}
  3. 规则 3:访问权限决定继承可见性

    子类继承了父类的所有属性和方法,但 private 成员因访问权限限制无法直接访问

    java
    class Parent {
        public String pubVar = "公共变量";
        protected String proVar = "保护变量";
        String defVar = "默认变量";
        private String priVar = "私有变量"; // 子类无法继承
    }
    
    class Child extends Parent {
        public void show() {
            System.out.println(pubVar); // ✅ 合法:继承public成员
            System.out.println(proVar); // ✅ 合法:继承protected成员
            System.out.println(defVar); // ✅ 合法:同包下继承default成员
            // System.out.println(priVar); // ❌ 错误:无法访问private成员
        }
    }

    如果子类要访问父类的 private 成员,需要通过父类的 非私有方法 访问:

    java
    class Parent {
        private String priVar = "私有变量"; // 子类无法继承
    
        // 1. 通过非私有方法(public/protected/default)访问私有属性
        public String getPriVar() {
            return this.priVar;
        }
    }
    
    class Child extends Parent {
        public void show() {
            getPriVar() // 2. 子类访问
        }
    }

快速入门

java
// 父类:Student
public class Student {
    // 共有属性
    public String name;
    public int age;
    private double score; // 私有属性

    // 共有方法
    public void setScore(double score) {
        this.score = score;
    }

    public void showInfo() {
        System.out.println("学生名" + name + " 年龄" + age + " 成绩" + score);
    }
}
java
// 子类:Pupil(小学生)
public class Pupil extends Student {
    // 特有方法
    public void testing() {
        System.out.println("小学生" + name + " 正在考小学数学..");
    }
}
java
// 子类:Graduate(大学生)
public class Graduate extends Student {
    // 特有方法
    public void testing() {
        System.out.println("大学生" + name + " 正在考大学数学..");
    }
}

成员变量的继承

成员变量的继承规则

  1. 子类继承父类非私有变量

    子类可直接使用父类的 public/protected/default 成员变量:

    java
    class Animal {
        protected String name; // 父类保护变量
        protected int age;
    }
    
    class Dog extends Animal {
        public void setInfo(String name, int age) {
            this.name = name; // 直接使用父类的 name
            this.age = age;   // 直接使用父类的 age
        }
    }
  2. 子类同名变量隐藏父类变量

    子类声明与父类同名的变量时,会 “隐藏” 父类的变量(而非覆盖),可通过 super 关键字访问父类变量:

    java
    class Parent {
        String name = "父类姓名";
    }
    
    class Child extends Parent {
        String name = "子类姓名"; // 隐藏父类 name
    
        public void show() {
            System.out.println(name); // 子类姓名(就近原则)
            System.out.println(super.name); // 父类姓名(通过super访问)
        }
    }

方法的继承

方法的继承

子类可直接调用父类的非私有方法:

java
class Animal {
    public void eat() {
        System.out.println("动物进食");
    }
}

class Cat extends Animal {
    public void play() {
        this.eat(); // 调用继承的父类方法
        System.out.println("猫玩球");
    }
}

方法重写

概述

方法重写(Override):子类定义与父类 “方法签名完全一致” 的方法,覆盖父类的原有逻辑。

方法签名:方法名 + 参数列表(参数个数、类型、顺序),返回值需兼容(Java 5+ 支持协变返回)。

方法重写规则

方法重写的核心规则必须遵守,否则编译报错

  • 方法签名一致:方法名、参数列表(个数 / 类型 / 顺序)必须完全相同
  • 返回值兼容:子类返回值需是父类返回值的子类(或相同)(协变返回)
  • 访问权限不降级:子类方法的访问权限不能比父类更严格(如父类 public → 子类不能 private)
  • 异常不扩大:子类方法抛出的异常不能比父类更广泛(如父类抛 RuntimeException → 子类不能抛 Exception)

示例:正确 vs 错误

java
// 父类
class Parent {
    public Object show(String msg) throws RuntimeException {
        System.out.println("父类方法:" + msg);
        return msg;
    }
}
java
// ✅ 子类:正确重写
class Child1 extends Parent {
    // 协变返回:Object → String(String是Object的子类)
    @Override // 注解:强制检查重写规则,推荐添加
    public String show(String msg) { // 父类抛RuntimeException,子类可不抛
        System.out.println("子类方法:" + msg);
        return msg;
    }
}
java
// ❌ 子类:错误重写(访问权限降级)
class Child2 extends Parent {
    // @Override // 编译报错:父类是public,子类是private
    private Object show(String msg) {
        return null;
    }
}

@Override

@Override 注解的作用

  • 强制编译器检查是否符合重写规则,避免拼写错误(如把 show 写成 shwo);
  • 增强代码可读性,明确标识该方法是重写父类的方法;
  • 推荐所有重写方法都添加该注解

构造方法的继承

构造方法的继承

构造方法不能被继承(构造方法名必须与类名一致,子类类名不同),但子类构造方法可通过 super() 调用父类构造方法。

核心规则

  1. 子类构造方法默认调用父类的无参构造方法super()),且 super() 必须是构造方法的第一条语句

    java
    class Parent {
        public Parent() { // 无参构造器
            System.out.println("父类的无参构造器默认会被调用");
        }
    }
    
    class Child extends Parent {
        public Child() {
            // super() 子类构造方法默认调用父类的无参构造方法
        }
    }
  2. 若父类没有无参构造(仅含带参构造),子类构造方法必须显式调用父类的带参构造(super(参数));

    java
    // 父类:仅含带参构造(无默认无参构造)
    class Parent {
        private String name;
    
        public Parent(String name) { // 带参构造
            this.name = name;
        }
    }
    java
    // 子类:必须显式调用父类带参构造
    class Child extends Parent {
        private int age;
    
        // ✅ 正确:显式调用父类带参构造
        public Child(String name, int age) {
            super(name); // 必须是第一条语句
            this.age = age;
        }
    
        // ❌ 错误:未调用父类构造(父类无无参构造)
        // public Child() {} // 编译报错
    }
  3. 不能同时调用 super()this()(二者都需是第一条语句)。

super

概述

super访问父类成员

super 是继承场景的核心关键字,作用是在子类中访问父类的成员(变量 / 方法 / 构造),与 this 对应:

  • this 指向当前对象。
  • super 指向父类对象

核心用法

super 的核心用法

  • super.成员变量:访问父类的非私有成员变量(解决子类同名变量的隐藏问题)
  • super.方法名(参数):调用父类的非私有原方法(子类重写后,仍可调用父类原方法)
  • super(参数):调用父类的构造方法(必须是构造方法第一条语句)

示例:super 的使用

java
// 父类
class Parent {
    String name = "父类";

    public Parent(String name) {
        this.name = name;
    }

    public void show() {
        System.out.println("父类show方法:" + name);
    }
}
java
// 子类
class Child extends Parent {
    String name = "子类";

    public Child(String parentName, String childName) {
        super(parentName); // 3. 调用父类带参构造
        this.name = childName;
    }

    @Override
    public void show() {
        super.show(); // 2. 调用父类的show方法
        System.out.println("子类show方法:" + this.name);
        System.out.println("父类name:" + super.name); // 1. 访问父类name
    }
}
java
// 测试
public class TestSuper {
    public static void main(String[] args) {
        Child child = new Child("父类姓名", "子类姓名");
        child.show();
        // 输出:
        // 父类show方法:父类姓名
        // 子类show方法:子类姓名
        // 父类name:父类姓名
    }
}

继承的访问修饰符

不同访问修饰符的成员在继承中的可见性,是继承的核心考点,以下是完整对比表:

父类成员修饰符同包子类可见不同包子类可见非子类可见子类能否继承
public
protected
default✅(同包)
private

关键说明

  • protected 是为继承设计的修饰符:不同包的子类可访问,但非子类不可访问;
  • default 仅同包的子类可继承,不同包的子类无法访问;
  • private 成员虽不能继承,但子类可通过父类的 public/protected 方法间接访问

继承的内存流程图

核心逻辑:子类对象创建后,会建立属性查找关系:

  1. 先查找子类是否有该属性,有则访问
  2. 若无,查找父类,有则访问(需满足访问权限)
  3. 若无,继续向上查找,直到 Object 类
  4. 注意:如果在查找的过程中遇到 private 属性,则不会在继续向上查找(即使上面有该属性并且可以访问),而是报错
java
// 爷类
class GrandPa {
    String name = "大头爷爷";
    String hobby = "旅游";
}

// 父类
class Father extends GrandPa {
    String name = "大头爸爸";
    private int age = 39; // 私有属性

    public int getAge() {
        return age;
    }
}

// 子类
class Son extends Father {
    String name = "大头儿子";
}

// 测试类
public class Test {
    public static void main(String[] args) {
        Son son = new Son();
        System.out.println(son.name); // 大头儿子(子类有name属性)
        System.out.println(son.hobby); // 旅游(子类无,父类无,爷类有)
        System.out.println(son.getAge()); // 39(父类私有属性,通过公共方法访问)
    }
}

内存流程图

练习

  • 练习 1

    java
    class A {
        A() { System.out.println("a"); }
        A(String name) { System.out.println("a name"); }
    }
    
    class B extends A {
        B() { this("abc"); System.out.println("b"); }
        B(String name) { System.out.println("b name"); } // 有一个默认的 super();
    }
    
    // main中:B b = new B(); 输出什么?
    // 输出结果:a → b name → b
  • 练习 2

    java
    class A {
        public A() {
            System.out.println("我是A类");
        }
    }
    
    class B extends A {
        public B() {
            System.out.println("我是B类的无参构造");
        }
    
        public B(String name) {
            System.out.println(name + "我是B类的有参构造");
        }
    }
    
    class C extends B {
        public C() {
            this("hello");
            System.out.println("我是c类的无参构造");
        }
    
        public C(String name) {
            super("hahah");
            System.out.println("我是c类的有参构造");
        }
    }
    
    public class ExtendsExercise02 {
        public static void main(String[] args) {
            C c = new C(); // 输出:我是A类 → hahah我是B类的有参构造 → 我是c类的有参构造 → 我是c类的无参构造
        }
    }
  • 练习 3

    需求:

    • 编写 Computer 类:属性 CPU、内存、硬盘;方法getDetails()返回详细信息
    • 编写 PC 子类:继承 Computer,添加特有属性 brand(品牌)
    • 编写 NotePad 子类:继承 Computer,添加特有属性 color(颜色)
    • 测试类:创建 PC 和 NotePad 对象,赋值并打印信息
    java
    // 父类:Computer
    public class Computer {
        private String cpu;
        private int memory;
        private int disk;
    
        public Computer(String cpu, int memory, int disk) {
            this.cpu = cpu;
            this.memory = memory;
            this.disk = disk;
        }
    
        public String getDetails() {
            return "cpu=" + cpu + " memory=" + memory + " disk=" + disk;
        }
    
        // getter/setter
        public String getCpu() { return cpu; }
        public void setCpu(String cpu) { this.cpu = cpu; }
        public int getMemory() { return memory; }
        public void setMemory(int memory) { this.memory = memory; }
        public int getDisk() { return disk; }
        public void setDisk(int disk) { this.disk = disk; }
    }
    
    // 子类:PC
    public class PC extends Computer {
        private String brand;
    
        public PC(String cpu, int memory, int disk, String brand) {
            super(cpu, memory, disk); // 调用父类构造器
            this.brand = brand;
        }
    
        public void printInfo() {
            System.out.println("PC信息=" + getDetails() + " brand=" + brand);
        }
    
        // getter/setter
        public String getBrand() { return brand; }
        public void setBrand(String brand) { this.brand = brand; }
    }
    
    // 测试类
    public class ExtendsExercise03 {
        public static void main(String[] args) {
            PC pc = new PC("intel", 16, 500, "IBM");
            pc.printInfo(); // PC信息=cpu=intel memory=16 disk=500 brand=IBM
        }
    }

super 关键字

基本介绍

super代表父类的引用,用于访问父类的属性、方法、构造器。

基本语法

  1. 访问父类属性:super.属性名;(不能访问父类 private 属性)
  2. 访问父类方法:super.方法名(参数列表);(不能访问父类 private 方法)
  3. 访问父类构造器:super(参数列表);(只能在子类构造器中使用,且在第一行)

super 的使用细节

  1. 当子类和父类有同名成员时,用super访问父类成员;无同名时,super可省略
  2. super的访问不限于直接父类,若爷爷类有同名成员,也可通过super访问(遵循就近原则)
  3. 调用父类构造器的好处:分工明确(父类属性由父类初始化,子类属性由子类初始化)

示例

java
package com.hspedu.super_;

// 爷爷类
public class Base {
    public int n1 = 999;
    public void cal() {
        System.out.println("Base类的cal()方法...");
    }
}

// 父类
public class A extends Base {
    protected int n2 = 200;
    int n3 = 300;
    private int n4 = 400;

    public void cal() {
        System.out.println("A类的cal()方法...");
    }
}

// 子类
public class B extends A {
    public int n1 = 888; // 与爷爷类同名

    public void test() {
        System.out.println("super.n1=" + super.n1); // 999(访问爷爷类的n1)
        System.out.println("super.n2=" + super.n2); // 200(访问父类的n2)
        super.cal(); // 调用父类的cal()方法 → A类的cal()方法...
    }
}

// 测试类
public class Super01 {
    public static void main(String[] args) {
        B b = new B();
        b.test();
    }
}

super 和 this 的比较

区别点thissuper
访问属性先访问本类,无则父类直接访问父类
调用方法先访问本类,无则父类直接访问父类
调用构造器调用本类构造器(必须在第一行)调用父类构造器(必须在第一行)
特殊含义代表当前对象代表父类对象的引用

方法重写/覆盖(override)

基本介绍

方法重写(override):子类有一个方法,与父类的方法名称、参数列表、返回类型完全一致(或子类返回类型是父类返回类型的子类),则子类方法重写了父类方法。

快速入门

java
package com.hspedu.override_;

// 父类
public class Animal {
    public void cry() {
        System.out.println("动物叫唤..");
    }

    public Object m1() {
        return null;
    }
}

// 子类
public class Dog extends Animal {
    // 重写父类的cry()方法
    @Override // 注解:标记方法重写(可选,推荐)
    public void cry() {
        System.out.println("小狗汪汪叫..");
    }

    // 重写父类的m1()方法(返回类型是父类返回类型的子类)
    @Override
    public String m1() {
        return null;
    }
}

// 测试类
public class Override01 {
    public static void main(String[] args) {
        Dog dog = new Dog();
        dog.cry(); // 小狗汪汪叫..(调用子类重写的方法)
    }
}

注意事项和使用细节

  1. 方法名、参数列表必须与父类完全一致
  2. 返回类型:
    • 子类返回类型与父类一致
    • 或子类返回类型是父类返回类型的子类(如父类返回 Object,子类返回 String)
  3. 访问权限:子类方法不能缩小父类方法的访问权限(public > protected > 默认 > private
  4. 父类 private 方法不能被重写(子类无法访问)
  5. 重写方法不能抛出比父类更多的异常(后续讲解)

课堂练习

练习 1:方法重写 vs 方法重载
名称发生范围方法名形参列表返回类型修饰符
重载(overload)本类必须相同类型/个数/顺序至少一个不同无要求无要求
重写(override)父子类必须相同完全相同相同或子类子类不能缩小访问范围
练习 2

需求

  • Person 类:属性 name、age(private);构造器;say()方法返回自我介绍
  • Student 类:继承 Person,添加属性 id、score(private);构造器;重写say()方法
java
package com.hspedu.override_;

// 父类:Person
public class Person {
    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String say() {
        return "name=" + name + " age=" + age;
    }

    // getter/setter
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
    public int getAge() { return age; }
    public void setAge(int age) { this.age = age; }
}

// 子类:Student
public class Student extends Person {
    private int id;
    private double score;

    public Student(String name, int age, int id, double score) {
        super(name, age); // 调用父类构造器
        this.id = id;
        this.score = score;
    }

    // 重写say()方法
    @Override
    public String say() {
        // 复用父类的say()方法,添加子类属性
        return super.say() + " id=" + id + " score=" + score;
    }

    // getter/setter
    public int getId() { return id; }
    public void setId(int id) { this.id = id; }
    public double getScore() { return score; }
    public void setScore(double score) { this.score = score; }
}

// 测试类
public class OverrideExercise {
    public static void main(String[] args) {
        Person jack = new Person("jack", 10);
        System.out.println(jack.say()); // name=jack age=10

        Student smith = new Student("smith", 20, 123456, 99.8);
        System.out.println(smith.say()); // name=smith age=20 id=123456 score=99.8
    }
}

多态

问题引入

需求:Master 类的 feed()方法实现主人给动物喂食物(Dog 吃 Bone、Cat 吃 Fish、Pig 吃 Rice)。

传统方法问题:每增加一种动物/食物,需新增 feed()方法,代码冗余,维护性差。

解决方案:使用多态统一管理。

多态基本介绍

多态(polymorphism):方法或对象具有多种形态,建立在封装和继承基础之上。

多态的具体体现

1) 方法的多态(重载和重写)

java
package com.hspedu.poly_;

public class PloyMethod {
    public static void main(String[] args) {
        // 方法重载体现多态(参数不同,调用不同方法)
        A a = new A();
        System.out.println(a.sum(10, 20)); // 30
        System.out.println(a.sum(10, 20, 30)); // 60

        // 方法重写体现多态(子类重写父类方法)
        B b = new B();
        a.say(); // A say() 方法被调用...
        b.say(); // B say() 方法被调用...
    }
}

class B { // 父类
    public void say() {
        System.out.println("B say() 方法被调用...");
    }
}

class A extends B { // 子类
    // 方法重载
    public int sum(int n1, int n2) {
        return n1 + n2;
    }

    public int sum(int n1, int n2, int n3) {
        return n1 + n2 + n3;
    }

    // 方法重写
    @Override
    public void say() {
        System.out.println("A say() 方法被调用...");
    }
}

2) 对象的多态(核心)

核心结论

  • 一个对象的编译类型和运行类型可以不一致
  • 编译类型:定义对象时确定(=左边),不能改变
  • 运行类型:创建对象时确定(=右边),可以改变

示例

java
package com.hspedu.poly_.objectpoly_;

// 父类
public class Animal {
    public void cry() {
        System.out.println("动物在叫....");
    }
}

// 子类:Dog
public class Dog extends Animal {
    @Override
    public void cry() {
        System.out.println("小狗汪汪叫...");
    }
}

// 子类:Cat
public class Cat extends Animal {
    @Override
    public void cry() {
        System.out.println("小猫喵喵叫...");
    }
}

// 测试类
public class PolyObject {
    public static void main(String[] args) {
        // 编译类型Animal,运行类型Dog
        Animal animal = new Dog();
        animal.cry(); // 小狗汪汪叫...(调用运行类型的方法)

        // 编译类型不变,运行类型改为Cat
        animal = new Cat();
        animal.cry(); // 小猫喵喵叫...(调用新的运行类型的方法)
    }
}

多态快速入门案例(主人喂食物)

java
package com.hspedu.poly_;

// 食物类(父类)
public class Food {
    private String name;

    public Food(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }
}

// 动物类(父类)
public class Animal {
    private String name;

    public Animal(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }
}

// 子类:Dog
public class Dog extends Animal {
    public Dog(String name) {
        super(name);
    }
}

// 子类:Bone
public class Bone extends Food {
    public Bone(String name) {
        super(name);
    }
}

// 主人类
public class Master {
    private String name;

    public Master(String name) {
        this.name = name;
    }

    // 多态方法:参数为父类类型,可接收子类对象
    public void feed(Animal animal, Food food) {
        System.out.println("主人" + name + " 给" + animal.getName() + " 吃" + food.getName());
    }
}

// 测试类
public class Poly01 {
    public static void main(String[] args) {
        Master master = new Master("韩顺平");
        Animal dog = new Dog("大黄");
        Food bone = new Bone("大骨头");
        master.feed(dog, bone); // 主人韩顺平 给大黄 吃大骨头

        // 新增Pig和Rice,无需修改feed()方法
        Animal pig = new Pig("小花");
        Food rice = new Rice("白米饭");
        master.feed(pig, rice); // 主人韩顺平 给小花 吃白米饭
    }
}

多态注意事项和细节

1) 多态的前提

  • 两个类存在继承关系
  • 子类重写父类方法

2) 多态向上转型(自动转换)

  • 语法:父类类型 引用名 = new 子类类型();
  • 特点:
    • 编译类型看左边,运行类型看右边
    • 可调用父类所有成员(需满足访问权限)
    • 不能调用子类特有成员

3) 多态向下转型(强制转换)

  • 语法:子类类型 引用名 = (子类类型) 父类引用;
  • 要求:
    • 父类引用必须指向当前子类类型的对象(否则报 ClassCastException)
    • 向下转型后,可调用子类特有成员

4) 属性没有重写之说

  • 属性的值看编译类型(与方法不同)

5) instanceof 关键字

  • 作用:判断对象的运行类型是否为指定类型或其子类型
  • 语法:对象 instanceof 类型 → 返回 boolean

示例代码

java
package com.hspedu.poly_.detail_;

public class PolyDetail {
    public static void main(String[] args) {
        // 向上转型
        Animal animal = new Cat();
        animal.eat(); // 猫吃鱼(子类重写的方法)
        // animal.catchMouse(); // 错误:不能调用子类特有方法

        // 向下转型(需先判断类型)
        if (animal instanceof Cat) {
            Cat cat = (Cat) animal;
            cat.catchMouse(); // 猫抓老鼠(子类特有方法)
        }

        // 属性看编译类型
        System.out.println(animal.name); // 动物(父类属性)
    }
}

class Animal {
    String name = "动物";
    public void eat() {
        System.out.println("吃");
    }
}

class Cat extends Animal {
    String name = "小猫";
    @Override
    public void eat() {
        System.out.println("猫吃鱼");
    }
    // 子类特有方法
    public void catchMouse() {
        System.out.println("猫抓老鼠");
    }
}

课堂练习

练习 1

判断以下代码正确性:

java
public class PolyExercise01 {
    public static void main(String[] args) {
        double d = 13.4;
        long l = (long) d; // 正确:强制类型转换
        System.out.println(l); // 13

        int in = 5;
        boolean b = (boolean) in; // 错误:boolean与int不能相互转换

        Object obj = "Hello";
        String objStr = (String) obj; // 正确:向下转型(obj运行类型是String)
        System.out.println(objStr); // Hello

        Object objPri = new Integer(5);
        String str = (String) objPri; // 错误:ClassCastException(运行类型是Integer)
        Integer str1 = (Integer) objPri; // 正确:向下转型
    }
}

Java 的动态绑定机制(重要)

动态绑定机制

  1. 当调用对象方法时,方法会与对象的运行类型绑定(而非编译类型)
  2. 当调用对象属性时,没有动态绑定机制,直接访问编译类型的属性

示例

java
package com.hspedu.poly_.dynamic_;

public class DynamicBinding {
    public static void main(String[] args) {
        A a = new B(); // 编译类型A,运行类型B
        System.out.println(a.sum()); // 40 → 解析:B的getI()返回20,20+20=40
        System.out.println(a.sum1()); // 30 → 解析:A的sum1()访问A的i=10,10+20=30
    }
}

class A { // 父类
    public int i = 10;

    public int sum() {
        return getI() + 20;
    }

    public int sum1() {
        return i + 20;
    }

    public int getI() {
        return i;
    }
}

class B extends A { // 子类
    public int i = 20;

    @Override
    public int getI() {
        return i;
    }
}

多态的应用

1) 多态数组

  • 数组定义类型为父类类型,存储子类对象
  • 访问子类特有方法需向下转型

示例

java
package com.hspedu.poly_.polyarr_;

// 父类:Person
public class Person {
    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String say() {
        return name + "\t" + age;
    }

    // getter/setter
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
    public int getAge() { return age; }
    public void setAge(int age) { this.age = age; }
}

// 子类:Student
public class Student extends Person {
    private double score;

    public Student(String name, int age, double score) {
        super(name, age);
        this.score = score;
    }

    @Override
    public String say() {
        return "学生" + super.say() + " score=" + score;
    }

    // 特有方法
    public void study() {
        System.out.println("学生" + getName() + " 正在学java...");
    }
}

// 子类:Teacher
public class Teacher extends Person {
    private double salary;

    public Teacher(String name, int age, double salary) {
        super(name, age);
        this.salary = salary;
    }

    @Override
    public String say() {
        return "老师" + super.say() + " salary=" + salary;
    }

    // 特有方法
    public void teach() {
        System.out.println("老师" + getName() + " 正在讲java课程...");
    }
}

// 测试类
public class PloyArray {
    public static void main(String[] args) {
        // 多态数组:存储Person、Student、Teacher对象
        Person[] persons = new Person[5];
        persons[0] = new Person("jack", 20);
        persons[1] = new Student("mary", 18, 100);
        persons[2] = new Student("smith", 19, 30.1);
        persons[3] = new Teacher("scott", 30, 20000);
        persons[4] = new Teacher("king", 50, 25000);

        // 遍历数组
        for (int i = 0; i < persons.length; i++) {
            System.out.println(persons[i].say()); // 动态绑定,调用子类重写的say()
            // 调用子类特有方法(向下转型)
            if (persons[i] instanceof Student) {
                ((Student) persons[i]).study();
            } else if (persons[i] instanceof Teacher) {
                ((Teacher) persons[i]).teach();
            }
        }
    }
}

2) 多态参数

  • 方法形参类型为父类类型,实参可传入子类对象
  • 统一处理不同子类对象,提高代码复用性

示例

java
package com.hspedu.poly_.polyparameter_;

// 父类:Employee
public class Employee {
    private String name;
    private double salary;

    public Employee(String name, double salary) {
        this.name = name;
        this.salary = salary;
    }

    // 计算年工资
    public double getAnnual() {
        return 12 * salary;
    }

    // getter/setter
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
    public double getSalary() { return salary; }
    public void setSalary(double salary) { this.salary = salary; }
}

// 子类:Manager
public class Manager extends Employee {
    private double bonus; // 奖金

    public Manager(String name, double salary, double bonus) {
        super(name, salary);
        this.bonus = bonus;
    }

    // 特有方法
    public void manage() {
        System.out.println("经理" + getName() + " is managing");
    }

    // 重写年工资计算(工资+奖金)
    @Override
    public double getAnnual() {
        return super.getAnnual() + bonus;
    }
}

// 子类:Worker
public class Worker extends Employee {
    public Worker(String name, double salary) {
        super(name, salary);
    }

    // 特有方法
    public void work() {
        System.out.println("普通员工" + getName() + " is working");
    }

    // 年工资直接复用父类方法
    @Override
    public double getAnnual() {
        return super.getAnnual();
    }
}

// 测试类
public class PloyParameter {
    public static void main(String[] args) {
        Worker tom = new Worker("tom", 2500);
        Manager milan = new Manager("milan", 5000, 200000);

        PloyParameter pp = new PloyParameter();
        pp.showEmpAnnual(tom); // 年工资:30000.0
        pp.showEmpAnnual(milan); // 年工资:260000.0

        pp.testWork(tom); // 普通员工tom is working
        pp.testWork(milan); // 经理milan is managing
    }

    // 多态参数:接收Employee及其子类对象
    public void showEmpAnnual(Employee e) {
        System.out.println("年工资:" + e.getAnnual());
    }

    // 调用子类特有方法
    public void testWork(Employee e) {
        if (e instanceof Worker) {
            ((Worker) e).work();
        } else if (e instanceof Manager) {
            ((Manager) e).manage();
        } else {
            System.out.println("不做处理...");
        }
    }
}

Object

equals 方法

== 和 equals 的对比(面试题)

特性==equals
适用类型基本类型、引用类型仅引用类型
基本类型判断判断值是否相等不支持
引用类型判断判断地址是否相等(是否同一对象)默认判断地址相等,子类可重写为判断内容相等

equals 源码分析

  • Object 类的 equals 方法(默认):
    java
    public boolean equals(Object obj) {
        return (this == obj); // 判断地址是否相等
    }
  • String 类重写 equals 方法(判断内容相等):
    java
    public boolean equals(Object anObject) {
        if (this == anObject) {
            return true;
        }
        if (anObject instanceof String) {
            String anotherString = (String) anObject;
            int n = value.length;
            if (n == anotherString.value.length) {
                char v1[] = value;
                char v2[] = anotherString.value;
                int i = 0;
                while (n-- != 0) {
                    if (v1[i] != v2[i])
                        return false;
                    i++;
                }
                return true;
            }
        }
        return false;
    }

示例

java
package com.hspedu.object_;

public class Equals01 {
    public static void main(String[] args) {
        // 基本类型 == 判断值相等
        int num1 = 10;
        double num2 = 10.0;
        System.out.println(num1 == num2); // true

        // 引用类型 == 判断地址相等
        String str1 = new String("hspedu");
        String str2 = new String("hspedu");
        System.out.println(str1 == str2); // false(不同对象,地址不同)
        System.out.println(str1.equals(str2)); // true(内容相同)

        // Integer重写equals方法
        Integer i1 = new Integer(1000);
        Integer i2 = new Integer(1000);
        System.out.println(i1 == i2); // false
        System.out.println(i1.equals(i2)); // true
    }
}

如何重写 equals 方法

需求:判断两个 Person 对象的 name、age、gender 是否完全相同,若相同返回 true。

java
package com.hspedu.object_;

public class EqualsExercise01 {
    public static void main(String[] args) {
        Person person1 = new Person("jack", 10, '男');
        Person person2 = new Person("jack", 10, '男');
        Person person3 = new Person("tom", 20, '女');

        System.out.println(person1.equals(person2)); // true
        System.out.println(person1.equals(person3)); // false
    }
}

class Person {
    private String name;
    private int age;
    private char gender;

    public Person(String name, int age, char gender) {
        this.name = name;
        this.age = age;
        this.gender = gender;
    }

    // 重写equals方法
    @Override
    public boolean equals(Object obj) {
        // 1. 判断是否是同一个对象
        if (this == obj) {
            return true;
        }

        // 2. 判断obj是否是Person类型
        if (obj instanceof Person) {
            // 3. 向下转型,获取obj的属性
            Person p = (Person) obj;
            // 4. 比较属性(字符串用equals,基本类型用==)
            return this.name.equals(p.name) && this.age == p.age && this.gender == p.gender;
        }

        // 5. 不是Person类型,返回false
        return false;
    }
}

课堂练习题

练习 1

java
package com.hspedu.object_;

public class EqualsExercise02 {
    public static void main(String[] args) {
        Person_ p1 = new Person_();
        p1.name = "hspedu";
        Person_ p2 = new Person_();
        p2.name = "hspedu";

        System.out.println(p1 == p2); // false(不同对象,地址不同)
        System.out.println(p1.name.equals(p2.name)); // true(字符串内容相同)
        System.out.println(p1.equals(p2)); // false(未重写equals,默认判断地址)

        String s1 = new String("asdf");
        String s2 = new String("asdf");
        System.out.println(s1.equals(s2)); // true(String重写了equals)
        System.out.println(s1 == s2); // false(地址不同)
    }
}

class Person_ {
    public String name;
}

练习 2

java
int it = 65;
float fl = 65.0f;
System.out.println("65 和65.0f 是否相等?" + (it == fl)); // true(基本类型值相等)

hashCode 方法

hashCode 方法定义

java
public int hashCode()

返回该对象的哈希值。支持此方法是为了提高哈希表(例如 java.util.Hashtable)的性能。

hashCode 的常规协定

  1. 在 Java 应用程序执行期间,对同一对象多次调用 hashCode 方法时,必须一致地返回相同的整数,前提是将对象用于 equals 比较的信息没有被修改。从应用程序的一次执行到同一应用程序的另一次执行,该整数无需保持一致。
  2. 如果根据 equals(Object) 方法,两个对象是相等的,那么对这两个对象中的每个对象调用 hashCode 方法都必须生成相同的整数结果。
  3. 如果根据 equals(java.lang.Object) 方法,两个对象不相等,那么对这两个对象中的任一对象调用 hashCode 方法不要求一定生成不同的整数结果。但是,程序员应该意识到,为不相等的对象生成不同整数结果可以提高哈希表的性能。

返回值

此对象的一个哈希码值。

老韩的 6 个小结

  1. 提高具有哈希结构的容器的效率!
  2. 两个引用,如果指向的是同一个对象,则哈希值肯定是一样的!
  3. 两个引用,如果指向的是不同对象,则哈希值是不一样的(非绝对,但推荐)
  4. 哈希值主要根据地址号生成,不能完全将哈希值等价于地址。
  5. 案例演示 HashCode_.javaobj.hashCode()(测试:A obj1 = new A(); A obj2 = new A(); A obj3 = obj1
  6. 后面在集合中,hashCode 如需使用会重写,讲解集合时将说明如何重写 hashCode() 代码。

案例代码(HashCode_.java)

java
package com.hspedu.object_;

public class HashCode_ {
    public static void main(String[] args) {
        AA aa = new AA();
        AA aa2 = new AA();
        AA aa3 = aa;
        System.out.println("aa.hashCode()=" + aa.hashCode());
        System.out.println("aa2.hashCode()=" + aa2.hashCode());
        System.out.println("aa3.hashCode()=" + aa3.hashCode());
    }
}

class AA {}

toString 方法

1) 基本介绍

  • 默认返回:全类名 + @ + 哈希值的十六进制(查看 ObjecttoString 方法)
  • 子类往往重写 toString 方法,用于返回对象的属性信息

2) 核心特性

  • 重写 toString 方法后,打印对象或拼接对象时,会自动调用该对象的 toString 形式
  • 当直接输出一个对象时,toString 方法会被默认调用(例如 System.out.println(monster); 等价于 monster.toString()

3) Object 的 toString() 源码解析

java
public String toString() {
    // (1) getClass().getName():类的全类名(包名+类名)
    // (2) Integer.toHexString(hashCode()):将对象的 hashCode 转为16进制字符串
    return getClass().getName() + "@" + Integer.toHexString(hashCode());
}

案例代码(ToString_.java)

java
package com.hspedu.object_;

public class ToString_ {
    public static void main(String[] args) {
        Monster monster = new Monster("小妖怪", "巡山的", 1000);
        System.out.println(monster.toString() + " hashcode=" + monster.hashCode());
        System.out.println("==当直接输出一个对象时,toString 方法会被默认的调用==");
        System.out.println(monster); // 等价于 monster.toString()
    }
}

class Monster {
    private String name;
    private String job;
    private double sal;

    public Monster(String name, String job, double sal) {
        this.name = name;
        this.job = job;
        this.sal = sal;
    }

    // 重写 toString 方法, 输出对象的属性(快捷键:alt+insert -> toString)
    @Override
    public String toString() {
        return "Monster{" +
                "name='" + name + '\'' +
                ", job='" + job + '\'' +
                ", sal=" + sal +
                '}';
    }

    @Override
    protected void finalize() throws Throwable {
        System.out.println("fin..");
    }
}

finalize 方法

1) 基本作用

当对象被回收时,系统自动调用该对象的 finalize 方法。子类可以重写该方法,做一些释放资源的操作(如关闭数据库连接、文件流等)。

2) 调用时机

当某个对象没有任何引用时,JVM 会将其标记为垃圾对象,随后垃圾回收机制(GC)会销毁该对象。在销毁对象前,会先调用 finalize 方法。

3) 垃圾回收机制说明

  • GC 的调用由系统决定(遵循自身算法)
  • 可通过 System.gc() 主动触发垃圾回收机制

老韩提示

实际开发中几乎不会使用 finalize,更多用于应付面试。

案例代码(Finalize_.java)

java
package com.hspedu.object_;

// 演示 Finalize 的用法
public class Finalize_ {
    public static void main(String[] args) {
        Car bmw = new Car("宝马");
        // 将对象置为垃圾对象
        bmw = null;
        System.gc(); // 主动调用垃圾回收器
        System.out.println("....");
    }
}

class Car {
    private String name; // 属性(资源)

    public Car(String name) {
        this.name = name;
    }

    // 重写 finalize
    @Override
    protected void finalize() throws Throwable {
        System.out.println("我们销毁汽车" + name);
        System.out.println("释放了某些资源...");
    }
}

断点调试(debug)

实际需求

  • 开发中新手查找错误时,可通过断点调试逐步查看源码执行过程,定位错误
  • 重要提示:断点调试是运行状态,以对象的运行类型执行(例:A extends B; B b = new A(); b.xx(); 按 A 类逻辑执行)

断点调试介绍

  1. 断点调试:在程序某一行设置断点,运行到该行时暂停,可逐步调试
  2. 核心作用:
    • 查看变量实时值
    • 定位错误代码行
    • 学习 Java 底层源码执行过程
  3. 必备技能:程序员必须掌握

断点调试的快捷键

快捷键功能描述
F7跳入方法内
F8逐行执行代码
Shift+F8跳出方法
F9 (resume)执行到下一个断点

断点调试应用案例

案例 1:逐行执行代码(Debug01.java)

java
package com.hspedu.debug_;

public class Debug01 {
    public static void main(String[] args) {
        // 演示逐行执行代码
        int sum = 0;
        for (int i = 0; i < 5; i++) {
            sum += i;
            System.out.println("i=" + i);
            System.out.println("sum=" + sum);
        }
    }
}

案例 2:数组越界异常调试(Debug02.java)

java
package com.hspedu.debug_;

public class Debug02 {
    public static void main(String[] args) {
        int[] arr = {1, 10, -1};
        // 数组长度为3,i<=3 会导致越界
        for (int i = 0; i <= arr.length; i++) {
            System.out.println(arr[i]);
        }
        System.out.println("退出 for");
    }
}

案例 3:查看 Arrays.sort 底层实现(Debug03.java)

java
package com.hspedu.debug_;

import java.util.Arrays;

public class Debug03 {
    public static void main(String[] args) {
        int[] arr = {1, -1, 10, -20, 100};
        // 调试查看 Arrays.sort 底层实现
        Arrays.sort(arr);
        for (int i = 0; i < arr.length; i++) {
            System.out.print(arr[i] + "\t");
        }
    }
}

案例 4:动态断点与 F9 用法(Debug04.java)

java
package com.hspedu.debug_;

import java.util.Arrays;

// 演示执行到下一个断点,支持动态下断点
public class Debug04 {
    public static void main(String[] args) {
        int[] arr = {1, -1, 10, -20, 100};
        Arrays.sort(arr);
        for (int i = 0; i < arr.length; i++) {
            System.out.print(arr[i] + "\t");
        }
        System.out.println("hello100");
        System.out.println("hello200");
        System.out.println("hello300");
        System.out.println("hello400");
        System.out.println("hello500");
        System.out.println("hello600");
        System.out.println("hello700");
    }
}

断点调试课后练习

  1. 使用断点调试追踪对象创建过程(Person[name,age,构造器..]
  2. 查看动态绑定机制的工作原理

项目-零钱通

项目开发流程说明

化繁为简,分步实现:

  1. 显示菜单并支持选择
  2. 零钱通明细
  3. 收益入账
  4. 消费
  5. 退出

项目需求说明

使用 Java 开发零钱通项目,支持功能:

  • 收益入账
  • 消费
  • 查看明细
  • 退出系统

项目界面

--零钱通菜单--
1 零钱通明细
2 收益入账
3 消费
4 退 出
请选择(1-4):1

--零钱通明细---
收益入账+100.0 2021-01-21 17:41 余额:100.0
收益入账+500.0 2021-01-21 17:41 余额:600.0
真功夫-20.0 2021-01-21 17:41 余额:580.0

项目代码实现(过程编程)

文件:SmallChangeSys.java

项目代码实现改进

  1. 退出确认:输入 4 退出时,提示"你确定要退出吗?y/n",需输入正确的 y/n
  2. 金额校验:收益入账和消费时,判断金额是否合理并提示
  3. 面向对象重构:
    • 编写 SmallChangeSysOOP.java 类(封装功能)
    • 编写 SmallChangeSysApp.java 完成测试(main 方法)

SmallChangeSysOOP 功能清单

  1. 显示菜单并选择
  2. 零钱通明细
  3. 收益入账
  4. 消费
  5. 退出

SmallChangeSysApp 测试代码框架

java
public class SmallChangeSysApp {
    public static void main(String[] args) {
        // 创建 SmallChangeSysOOP 对象,调用相关方法完成功能
        SmallChangeSysOOP smallChangeSys = new SmallChangeSysOOP();
        smallChangeSys.mainMenu();
    }
}

本章作业

Homework01.java

定义 Person 类(name,age,job),初始化 3 个 Person 对象的数组,按 age 从大到小排序(使用冒泡排序)。

Homework02.java

写出四种访问修饰符和各自的访问权限。

Homework03.java

  1. 编写老师类 Teacher,属性:姓名name年龄age职称post基本工资salary
  2. 业务方法 introduce():输出教师信息
  3. 编写三个子类:
    • Professor(教授):工资级别 1.3
    • AssociateProfessor(副教授):工资级别 1.2
    • Lecturer(讲师):工资级别 1.1
  4. 子类重写 introduce() 方法
  5. 初始化老师对象,调用 introduce() 打印信息

Homework04.java

通过继承实现员工工资核算打印功能:

  • 父类:Employee(员工类)
  • 子类:Manager(部门经理)、Worker(普通员工)
  • 工资计算规则:
    1. 部门经理工资 = 1000 + 单日工资 × 天数 × 1.2(奖金+基本工资)
    2. 普通员工工资 = 单日工资 × 天数 × 1.0(基本工资)
  • 员工属性:姓名、单日工资、工作天数
  • 员工方法:打印工资(子类重写)
  • 初始化对象并调用打印工资方法

Homework05.java(包:com.hspedu.homework.homework5)

设计继承体系:

  • 父类:Employee(员工类)
  • 子类:Worker(工人)、Peasant(农民)、Teacher(教师)、Scientist(科学家)、Waiter(服务生)
  • 工资规则:
    1. 工人、农民、服务生:只有基本工资 sal
    2. 教师:基本工资 + 课酬(classDay 天 × classSal 元/天)
    3. 科学家:基本工资 + 年终奖 bonus
  • 测试类:打印各类员工的全年工资

Homework06.java

已知 GrandFatherSon 在同一个包,分析父类和子类中通过 thissuper 可调用的属性和方法:

java
class Grand { // 超类
    String name = "AA";
    private int age = 100;
    public void g1() {}
}

class Father extends Grand { // 父类
    String id = "001";
    private double score;
    public void f1() {}
}

class Son extends Father { // 子类
    String name = "BB";
    public void g1() {}
    private void show() {
        // super 可访问哪些成员?
        // this 可访问哪些成员?
    }
}

Homework07.java

写出程序运行结果:

java
class Test { // 父类
    String name = "Rose";
    Test() {
        System.out.println("Test");
    }
    Test(String name) {
        this.name = name;
    }
    public void test() {
        System.out.println(super.name);
        System.out.println(this.name);
    }
}

class Demo extends Test { // 子类
    String name = "Jack";
    Demo() {
        super();
        System.out.println("Demo");
    }
    Demo(String s) {
        super(s);
    }
    public static void main(String[] args) {
        new Demo().test(); // 匿名对象
        new Demo("john").test(); // 匿名对象
    }
}

Homework08.java

扩展 BankAccount 类:

java
class BankAccount {
    private double balance; // 余额
    public BankAccount(double initialBalance) {
        this.balance = initialBalance;
    }
    // 存款
    public void deposit(double amount) {
        balance += amount;
    }
    // 取款
    public void withdraw(double amount) {
        balance -= amount;
    }
    // set 和 getBalance 方法...
}
  1. 扩展 CheckingAccount 类:每次存款和取款收取 1 美元手续费
  2. 扩展 SavingsAccount 类:
    • 每月产生利息(调用 earnMonthlyInterest 方法)
    • 每月 3 次免手续费的存款/取款
    • earnMonthlyInterest 方法中重置交易计数
  3. 体会重写的好处

Homework09.java

设计类:

  1. Point 类:xy 坐标(构造器提供)
  2. 子类 LabeledPoint:构造器接受标签值和 x,y 坐标(例:new LabeledPoint("Black",1929,230.07)

Homework10.java

编写 Doctor 类(name,age,job,gender,sal):

  1. 提供 getter/setter 方法
  2. 5 个参数的构造器
  3. 重写 Object 类的 equals 方法:判断属性是否完全相同
  4. 测试类中创建两个对象,判断是否相等

Homework11.java

写出对象向上转型和向下转型的代码,及可调用的方法:

java
class Person { // 父类
    public void run() {
        System.out.println("person run");
    }
    public void eat() {
        System.out.println("person eat");
    }
}

class Student extends Person { // 子类
    public void study() {
        System.out.println("student study.");
    }
    @Override
    public void run() {
        System.out.println("student run");
    }
}

Homework12.java

简述 ==equals 的区别:

名称概念用于基本数据类型用于引用类型
==比较运算符可以,判断值相等可以,判断对象地址是否相等
equalsObject 类的方法不可以默认判断地址相等;子类常重写为判断属性相等(如 StringInteger

Homework13.java

按要求实现类并打印效果:

打印效果

老师的信息:
姓名:张飞
年龄:30
性别:男
工龄:5
我承诺,我会认真教课。
张飞爱玩象棋

学生的信息:
姓名:小明
年龄:15
性别:男
学号:00023102
我承诺,我会好好学习。
小明爱玩足球。

需求

  1. Student 类:name,sex,age,stu_id,封装,构造器赋值
  2. Teacher 类:name,sex,age,work_age,封装,构造器赋值
  3. 抽取父类 Person,封装共同属性和方法
  4. Studentstudy() 方法:输出"我承诺,我会好好学习。"
  5. Teacherteach() 方法:输出"我承诺,我会认真教学。"
  6. 父子类共有 play() 方法(父类定义,子类重写):
    • 学生返回"xx 爱玩足球"
    • 老师返回"xx 爱玩象棋"
  7. 定义多态数组(2 个学生+2 个教师),按年龄从高到低排序
  8. 定义方法(形参为 Person 类型):调用学生的 study() 或教师的 teach()

Homework14.java

程序阅读题:执行 C c = new C(); 输出什么?

java
class A { // 超类
    public A() {
        System.out.println("我是A类");
    }
}

class B extends A { // 父类
    public B() {
        System.out.println("我是B类的无参构造");
    }
    public B(String name) {
        System.out.println(name + "我是B类的有参构造");
    }
}

class C extends B { // 子类
    public C() {
        this("hello");
        System.out.println("我是c类的无参构造");
    }
    public C(String name) {
        super("hahah");
        System.out.println("我是c类的有参构造");
    }
}

Homework15.java

  1. 什么是多态?
  2. 多态的具体体现(举例说明):
    • 方法多态(重载、重写)
    • 对象多态(编译类型与运行类型不一致)

Homework16.java

什么是 Java 的动态绑定机制?

  1. 调用对象方法时,方法与对象的内存地址/运行类型绑定
  2. 调用对象属性时,无动态绑定机制,哪里声明哪里使用